package org.jivesoftware.smack.proxy;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.SocketFactory;

/**
 * Socket factory for Socks5 proxy
 * 
 * @author Atul Aggarwal
 */
public class Socks5ProxySocketFactory extends SocketFactory {
	private ProxyInfo proxy;

	public Socks5ProxySocketFactory(ProxyInfo proxy) {
		this.proxy = proxy;
	}

	public Socket createSocket(String host, int port) throws IOException,
			UnknownHostException {
		return socks5ProxifiedSocket(host, port);
	}

	public Socket createSocket(String host, int port, InetAddress localHost,
			int localPort) throws IOException, UnknownHostException {

		return socks5ProxifiedSocket(host, port);

	}

	public Socket createSocket(InetAddress host, int port) throws IOException {

		return socks5ProxifiedSocket(host.getHostAddress(), port);

	}

	public Socket createSocket(InetAddress address, int port,
			InetAddress localAddress, int localPort) throws IOException {

		return socks5ProxifiedSocket(address.getHostAddress(), port);

	}

	private Socket socks5ProxifiedSocket(String host, int port)
			throws IOException {
		Socket socket = null;
		InputStream in = null;
		OutputStream out = null;
		String proxy_host = proxy.getProxyAddress();
		int proxy_port = proxy.getProxyPort();
		String user = proxy.getProxyUsername();
		String passwd = proxy.getProxyPassword();

		try {
			socket = new Socket(proxy_host, proxy_port);
			in = socket.getInputStream();
			out = socket.getOutputStream();

			socket.setTcpNoDelay(true);

			byte[] buf = new byte[1024];
			int index = 0;

			/*
			 * +----+----------+----------+ |VER | NMETHODS | METHODS |
			 * +----+----------+----------+ | 1 | 1 | 1 to 255 |
			 * +----+----------+----------+
			 * 
			 * The VER field is set to X'05' for this version of the protocol.
			 * The NMETHODS field contains the number of method identifier
			 * octets that appear in the METHODS field.
			 * 
			 * The values currently defined for METHOD are:
			 * 
			 * o X'00' NO AUTHENTICATION REQUIRED o X'01' GSSAPI o X'02'
			 * USERNAME/PASSWORD o X'03' to X'7F' IANA ASSIGNED o X'80' to X'FE'
			 * RESERVED FOR PRIVATE METHODS o X'FF' NO ACCEPTABLE METHODS
			 */

			buf[index++] = 5;

			buf[index++] = 2;
			buf[index++] = 0; // NO AUTHENTICATION REQUIRED
			buf[index++] = 2; // USERNAME/PASSWORD

			out.write(buf, 0, index);

			/*
			 * The server selects from one of the methods given in METHODS, and
			 * sends a METHOD selection message:
			 * 
			 * +----+--------+ |VER | METHOD | +----+--------+ | 1 | 1 |
			 * +----+--------+
			 */
			// in.read(buf, 0, 2);
			fill(in, buf, 2);

			boolean check = false;
			switch ((buf[1]) & 0xff) {
			case 0: // NO AUTHENTICATION REQUIRED
				check = true;
				break;
			case 2: // USERNAME/PASSWORD
				if (user == null || passwd == null) {
					break;
				}

				/*
				 * Once the SOCKS V5 server has started, and the client has
				 * selected the Username/Password Authentication protocol, the
				 * Username/Password subnegotiation begins. This begins with the
				 * client producing a Username/Password request:
				 * 
				 * +----+------+----------+------+----------+ |VER | ULEN |
				 * UNAME | PLEN | PASSWD |
				 * +----+------+----------+------+----------+ | 1 | 1 | 1 to 255
				 * | 1 | 1 to 255 | +----+------+----------+------+----------+
				 * 
				 * The VER field contains the current version of the
				 * subnegotiation, which is X'01'. The ULEN field contains the
				 * length of the UNAME field that follows. The UNAME field
				 * contains the username as known to the source operating
				 * system. The PLEN field contains the length of the PASSWD
				 * field that follows. The PASSWD field contains the password
				 * association with the given UNAME.
				 */
				index = 0;
				buf[index++] = 1;
				buf[index++] = (byte) (user.length());
				System.arraycopy(user.getBytes(), 0, buf, index, user.length());
				index += user.length();
				buf[index++] = (byte) (passwd.length());
				System.arraycopy(passwd.getBytes(), 0, buf, index,
						passwd.length());
				index += passwd.length();

				out.write(buf, 0, index);

				/*
				 * The server verifies the supplied UNAME and PASSWD, and sends
				 * the following response:
				 * 
				 * +----+--------+ |VER | STATUS | +----+--------+ | 1 | 1 |
				 * +----+--------+
				 * 
				 * A STATUS field of X'00' indicates success. If the server
				 * returns a `failure' (STATUS value other than X'00') status,
				 * it MUST close the connection.
				 */
				// in.read(buf, 0, 2);
				fill(in, buf, 2);
				if (buf[1] == 0) {
					check = true;
				}
				break;
			default:
			}

			if (!check) {
				try {
					socket.close();
				} catch (Exception eee) {
				}
				throw new ProxyException(ProxyInfo.ProxyType.SOCKS5,
						"fail in SOCKS5 proxy");
			}

			/*
			 * The SOCKS request is formed as follows:
			 * 
			 * +----+-----+-------+------+----------+----------+ |VER | CMD |
			 * RSV | ATYP | DST.ADDR | DST.PORT |
			 * +----+-----+-------+------+----------+----------+ | 1 | 1 | X'00'
			 * | 1 | Variable | 2 |
			 * +----+-----+-------+------+----------+----------+
			 * 
			 * Where:
			 * 
			 * o VER protocol version: X'05' o CMD o CONNECT X'01' o BIND X'02'
			 * o UDP ASSOCIATE X'03' o RSV RESERVED o ATYP address type of
			 * following address o IP V4 address: X'01' o DOMAINNAME: X'03' o IP
			 * V6 address: X'04' o DST.ADDR desired destination address o
			 * DST.PORT desired destination port in network octet order
			 */

			index = 0;
			buf[index++] = 5;
			buf[index++] = 1; // CONNECT
			buf[index++] = 0;

			byte[] hostb = host.getBytes();
			int len = hostb.length;
			buf[index++] = 3; // DOMAINNAME
			buf[index++] = (byte) (len);
			System.arraycopy(hostb, 0, buf, index, len);
			index += len;
			buf[index++] = (byte) (port >>> 8);
			buf[index++] = (byte) (port & 0xff);

			out.write(buf, 0, index);

			/*
			 * The SOCKS request information is sent by the client as soon as it
			 * has established a connection to the SOCKS server, and completed
			 * the authentication negotiations. The server evaluates the
			 * request, and returns a reply formed as follows:
			 * 
			 * +----+-----+-------+------+----------+----------+ |VER | REP |
			 * RSV | ATYP | BND.ADDR | BND.PORT |
			 * +----+-----+-------+------+----------+----------+ | 1 | 1 | X'00'
			 * | 1 | Variable | 2 |
			 * +----+-----+-------+------+----------+----------+
			 * 
			 * Where:
			 * 
			 * o VER protocol version: X'05' o REP Reply field: o X'00'
			 * succeeded o X'01' general SOCKS server failure o X'02' connection
			 * not allowed by ruleset o X'03' Network unreachable o X'04' Host
			 * unreachable o X'05' Connection refused o X'06' TTL expired o
			 * X'07' Command not supported o X'08' Address type not supported o
			 * X'09' to X'FF' unassigned o RSV RESERVED o ATYP address type of
			 * following address o IP V4 address: X'01' o DOMAINNAME: X'03' o IP
			 * V6 address: X'04' o BND.ADDR server bound address o BND.PORT
			 * server bound port in network octet order
			 */

			// in.read(buf, 0, 4);
			fill(in, buf, 4);

			if (buf[1] != 0) {
				try {
					socket.close();
				} catch (Exception eee) {
				}
				throw new ProxyException(ProxyInfo.ProxyType.SOCKS5,
						"server returns " + buf[1]);
			}

			switch (buf[3] & 0xff) {
			case 1:
				// in.read(buf, 0, 6);
				fill(in, buf, 6);
				break;
			case 3:
				// in.read(buf, 0, 1);
				fill(in, buf, 1);
				// in.read(buf, 0, buf[0]+2);
				fill(in, buf, (buf[0] & 0xff) + 2);
				break;
			case 4:
				// in.read(buf, 0, 18);
				fill(in, buf, 18);
				break;
			default:
			}
			return socket;

		} catch (RuntimeException e) {
			throw e;
		} catch (Exception e) {
			try {
				if (socket != null) {
					socket.close();
				}
			} catch (Exception eee) {
			}
			String message = "ProxySOCKS5: " + e.toString();
			if (e instanceof Throwable) {
				throw new ProxyException(ProxyInfo.ProxyType.SOCKS5, message,
						(Throwable) e);
			}
			throw new IOException(message);
		}
	}

	private void fill(InputStream in, byte[] buf, int len) throws IOException {
		int s = 0;
		while (s < len) {
			int i = in.read(buf, s, len - s);
			if (i <= 0) {
				throw new ProxyException(ProxyInfo.ProxyType.SOCKS5, "stream "
						+ "is closed");
			}
			s += i;
		}
	}
}
